function TextLayout(textModel){
    this.init$Eventable();
    this.$textModel = textModel;
    this.on(this.$textModel, 'Changed', this._onModelChanged);
    // run: [length, kind].
    this.$allRuns = [];
    this.$quickRunsOffset = [];
    this.$rowsEndColumn = [];
    this.$maxColumn = 0;
    this.$tabSize = 4;
    this.$useWordWrap = false;
    this.$wrapLimit = 80;
    this.$wrapData = null;
}

var _splitRegex = /([\t]+)|([\u1100-\u115F\u11A3-\u11A7\u11FA-\u11FF\u2329-\u232A\u2E80-\u2E99\u2E9B-\u2EF3\u2F00-\u2FD5\u2FF0-\u2FFB\u3000-\u303E\u3041-\u3096\u3099-\u30FF\u3105-\u312D\u3131-\u318E\u3190-\u31BA\u31C0-\u31E3\u31F0-\u321E\u3220-\u3247\u3250-\u32FE\u3300-\u4DBF\u4E00-\uA48C\uA490-\uA4C6\uA960-\uA97C\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFAFF\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE66\uFE68-\uFE6B\uFF01-\uFF60\uFFE0-\uFFE6]+)|[\v\f\uFEFF\u200B\u200C\u200D]+/g;
var _KIND_ZERO = 0;
var _KIND_CHAR = 1;
var _KIND_CJK = 2;
var _KIND_TAB = 4;
var _KIND_FOLLOWTAB = 5;
var _FLOOR = -1;
var _ROUND = 0;
var _CEIL = 1;

TextLayout.prototype = {
    _update: function (){
        // 更新runs信息
        var text = this.$textModel.getText();
        var allRuns = [];
        
        var lastIndex = 0;
        var match;
        _splitRegex.lastIndex = 0;
        while (match = _splitRegex.exec(text)) {
            var index = match.index;
            if (index > lastIndex) {
                allRuns.push([index - lastIndex, _KIND_CHAR]);
            }
            lastIndex = _splitRegex.lastIndex;
            if (match[1]) {
                var tabs = match[0];
                // 将连续的tabs拆分成前后部分以方便计算. 因为后面的部分每个tab的占位数都是固定的.
                allRuns.push([1, _KIND_TAB]);
                if (tabs.length > 1) {
                    allRuns.push([tabs.length - 1, _KIND_FOLLOWTAB]);
                }
            } else {
                allRuns.push([match[0].length, match[2] ? _KIND_CJK : _KIND_ZERO]);
            }
        }
        if (_splitRegex.lastIndex !== text.length) {
            allRuns.push([text.length - _splitRegex.lastIndex, _KIND_CHAR]);
        }

        // 确保allRuns不为空. 以使_getRuns等方法不需要多余的判断.
        if (!allRuns.length) {
            allRuns.push([0, _KIND_ZERO]);
        }

//        var stepOffset = 0;
//        for (var i=0; i<allRuns.length; i++) {
//            var run = allRuns[i];
//            if (i % 512 == 0) {
//                this.$quickRunsOffset[i / 512] = stepOffset;
//            }
//            stepOffset += run[0];
//        }
        this.$allRuns = allRuns;

        // 更新column信息
        var rowsEndColumn = [];
        var count = this.getRowCount();
        for (var row=0; row<count; row++) {
            rowsEndColumn[row] = this._computeColumn(this.getRowEndOffset(row), this.getRowStartOffset(row));
        }
        this.$rowsEndColumn = rowsEndColumn;
        this.$maxColumn = Math.max(0, Math.max.apply(Math, rowsEndColumn));
    },
    _findRun: function (offset, runIndex, runOffset){
        // 需要维护一个快查表方便快速定位. 否则遭遇10万行以上的并且几乎每行都带tab时, 性能可想而知.
        var allRuns = this.$allRuns;
        runIndex = runIndex | 0;
        runOffset = runOffset | 0;
        for (; runIndex<allRuns.length; runIndex++) {
            var run = allRuns[runIndex];
            if (offset <= runOffset + run[0]) {
                break;
            }
            runOffset += run[0];
        }
        return [runIndex, runOffset];
    },
    _getRuns: function (startOffset, endOffset){
        var allRuns = this.$allRuns;
        var run;
        var runs = [];
        var r1 = this._findRun(startOffset);
        var startRunIndex = r1[0];
        var startRunOffset = r1[1];
        var r2 = this._findRun(endOffset, startRunIndex, startRunOffset);
        var endRunIndex = r2[0];
        var endRunOffset = r2[1];
        run = allRuns[startRunIndex];
        if (startRunIndex === endRunIndex) {
            runs.push([endOffset - startOffset, run[1]]);
        } else {
            if (startOffset >= startRunOffset) {
                runs.push([run[0] + startRunOffset - startOffset, run[1]]);
            }
            for (var runIndex=startRunIndex+1; runIndex<endRunIndex; runIndex++) {
                runs.push(allRuns[runIndex]);
            }
            if (endRunOffset !== endOffset) {
                run = allRuns[endRunIndex];
                runs.push([endOffset - endRunOffset, run[1]]);
            }
        }
        return runs;
    },
    _getRunColumn: function (run, column){
        column = column | 0;
        switch (run[1]) {
        case _KIND_CHAR: return run[0];
        case _KIND_CJK: return run[0] * 2;
        case _KIND_FOLLOWTAB: return run[0] * 4;
        case _KIND_TAB: return this.$tabSize - column % this.$tabSize;
        default: return 0;
        }
    },
    _getRunsColumn: function (runs, column){
        column = column | 0;
        for (var i=0; i<runs.length; i++) {
            column += this._getRunColumn(runs[i]);
        }
        return column;
    },
    _computeColumn: function (offset, startOffset){
        return this._getRunsColumn(this._getRuns(startOffset | 0, offset));
    },
    getRowCount: function (){
        return this.$textModel.getLineCount();
    },
    getRow: function (lineIndex){
        return lineIndex;
    },
    getRowLineIndex: function (row){
        return row;
    },
    getRowEndColumn: function (row){
        return this.$rowsEndColumn[row];
    },
    getMaxColumn: function (){
        return this.$maxColumn;
    },
    getRowAndColumn: function (offset){
        var left = 0;
        var right = this.getRowCount();
        while (left < right) {
            var middle = (left + right) >> 1;
            var next = middle + 1;
            var middleStartOffset = this.getRowStartOffset(middle);
            var nextRowStartOffset = this.getRowStartOffset(next);
            if (offset < middleStartOffset) {
                right = middle;
            } else if (offset > nextRowStartOffset) {
                left = next;
            } else if (offset == nextRowStartOffset) {
                left = right = next;
            } else {
                left = right = middle;
            }
        }
        var row = left;
        // 因为之前的判断offset == nextRowStartOffset可能在最后一行时会出问题. 所以这里修一下.
        if (row >= this.getRowCount()) {
            row = this.getRowCount() - 1;
        }
        var rowStartOffset = this.getRowStartOffset(row);
        var column = this._computeColumn(offset, rowStartOffset);
        return [row, column];
    },
    getRowStartOffset: function (row){
        return this.$textModel.getLineStartOffset(row);
    },
    getRowEndOffset: function (row){
        return this.$textModel.getLineEndOffset(row);
    },
    getRowColumnOffset: function (row, column, mode){
        mode = mode | 0; // 默认为_ROUND.
        if (column == 0) {
            return this.getRowStartOffset(row);
        } else if (column >= this.getRowEndColumn(row)) {
            return this.getRowEndOffset(row);
        }
        var allRuns = this.$allRuns;
        var rowStartOffset = this.getRowStartOffset(row);
        var r = this._findRun(rowStartOffset);
        var runIndex = r[0];
        var runOffset = r[1];
        var run = allRuns[runIndex];
        run = run[rowStartOffset - runOffset, run[1]];
        var runColumn;
        var stepColumn = 0;
        var stepOffset = rowStartOffset;
        var endRunIndex = allRuns.length;
        for (;runIndex < endRunIndex; runIndex++) {
            run = allRuns[runIndex];
            runColumn = this._getRunColumn(run, stepColumn);
            if (stepColumn + runColumn > column) {
                break;
            }
            stepOffset += run[0];
            stepColumn += runColumn;
        }
        var deltaOffset;
        if (run[0] === _KIND_ZERO) {
            deltaOffset = 0;
        } else if (run[0] === _KIND_TAB) {
            deltaOffset = mode === _CEIL ? 1 :
                mode === _FLOOR ? 0 : column >= (stepColumn + stepColumn + runColumn) / 2 ? 1 : 0;
        } else {
            var fn = mode === _CEIL ? Math.ceil :
                mode === _FLOOR ? Math.floor : Math.round;
            deltaOffset = fn.call(Math, (column - stepColumn) / (run[0] === _KIND_CJK ? 2 : run[0] === _KIND_FOLLOWTAB ? 4 : 1));
        }
        return stepOffset + deltaOffset;
    },
    renderInlineHTML: function (buff, startOffset, endOffset, fontInfo, column){
        var runs = this._getRuns(startOffset, endOffset);
        var stepColumn = column == null ? this.getRowAndColumn(startOffset)[1] : column;
        var stepOffset = startOffset;
        for (var i=0; i<runs.length; i++) {
            var run = runs[i];
            var runColumn = this._getRunColumn(run, stepColumn);
            if (run[1] == _KIND_TAB || run[1] == _KIND_FOLLOWTAB) {
                buff.push(Utils.stringRepeat('&#160;', runColumn));
            } else {
                var text = this.$textModel.getText(stepOffset, stepOffset + run[0]);
                if (run[1] == _KIND_CJK) {
                    var letterSpacing = fontInfo.__letterSpacing || (
                        fontInfo.__letterSpacing = fontInfo.cjkCharWidth - fontInfo.charWidth * 2);
                    buff.push('<span style="letter-spacing:', letterSpacing,
                        'px;padding-left:', letterSpacing >> 1,
                        'px;">',
                        text, '</span>');
                } else if (run[1] == _KIND_CHAR) {
                    buff.push(Utils.escapeHTML(text));
                }
            }
            stepOffset += run[0];
            stepColumn += runColumn;
        }
        return stepColumn;
    },
    _onModelChanged: function (){
        this._update();
        this.dispatchEvent({
            type: 'LayoutChanged'
        });
    }
};

Utils.extend(TextLayout, Eventable);